using System;
using System.Data;
using System.Collections.Generic;
using System.Transactions;
using System.Data.Common;

using SBPweb.Data.Generic;
using SBPweb.Data.SQL2005;

namespace SBPweb.Data
{
	
	/// <summary>
	/// This class implements transaction handling for sql and web repositories
	/// </summary>
	public class Transaction
	{

		#region Private members

		private Guid mTransactionId;
		private string mSessionId;
		private TransactionState mState;
		private TransactionType mType;
		private SqlConnectionProvider mProvider;

		private TransactionScope mTransactionScope;
        private List<SqlRepository<Entity>> mSqlRepositories;
        private List<Entity> mSqlEntities;
		private List<IRepository<IEntity>> mWebRepositories;
		private List<KeyValuePair<IEntity, IRepository<IEntity>>> mWebEntities;

		#endregion

		#region Constructor logic

		/// <summary>
		/// Initializes a new Transaction instance
		/// </summary>
		/// <param name="sessionId">string sessionId</param>
		public Transaction(string sessionId)
		{
			Initialize(sessionId, Guid.NewGuid());
		}

		/// <summary>
		/// Initializes a new Transaction instance
		/// </summary>
		/// <param name="sessionId">string sessionId</param>
		/// <param name="guid">Guid guid</param>
		public Transaction(string sessionId, Guid guid)
		{
			Initialize(sessionId, guid);
		}

		private void Initialize(string sessionId, Guid guid)
		{
			mState = TransactionState.Idle;
			mSessionId = sessionId;
			mTransactionId = guid;
			mTransactionScope = null;
			mWebRepositories = new List<IRepository<IEntity>>();
			mWebEntities = new List<KeyValuePair<IEntity, IRepository<IEntity>>>();
			mSqlRepositories = new List<SqlRepository<Entity>>();
            mSqlEntities = new List<Entity>();
			mProvider = new SqlConnectionProvider();
		}

		/// <summary>
		/// Finalizes the transaction
		/// </summary>
		public void Terminate()
		{
			try
			{
				if (mState == TransactionState.Started)
				{
					Rollback();
				}
			}
			finally
			{
				if (mTransactionScope != null)
				{
					mTransactionScope.Dispose();
					mTransactionScope = null;
				}
				mWebRepositories.Clear();
				mWebEntities.Clear();
				mSqlRepositories.Clear();
				mSqlEntities.Clear();
				mState = TransactionState.Terminated;
			}
		}

		#endregion

		#region Public property accessors

		/// <summary>
		/// Returns the Guid of the transaction
		/// </summary>
		public Guid TransactionId
		{
			get
			{
				return mTransactionId;
			}
		}

		/// <summary>
		/// Returns the state of the transaction
		/// </summary>
		public TransactionState State
		{
			get
			{
				return mState;
			}
		}

		#endregion

		#region Public methods

		/// <summary>
		/// Registers a web entity and its repository
		/// </summary>
		/// <param name="webRepository">IRepository&lt;IEntity&gt; webRepository</param>
		/// <param name="webEntity">IEntity webEntity</param>
		public void RegisterWebEntity(IRepository<IEntity> webRepository, IEntity webEntity)
		{
			if (webEntity == null)
			{
				throw new ArgumentNullException("webEntity");
			}
            if (webEntity.ActionState == EntityActionState.Detached)
            {
                throw new ArgumentException("Cannot register SqlEntity, because it's detached.");
            }
            if (mState == TransactionState.Idle || mState == TransactionState.Started)
			{
				try
				{
					KeyValuePair<IEntity, IRepository<IEntity>> kvp = GetWebKeyValuePair(webRepository, webEntity);
					AddWebEntityPair(kvp);
				}
				catch (Exception ex)
				{
					LogManager.WriteWarning(this, this.ToString() + ex.ToString(), "Transaction");
					throw ex;
				}
			}
			else
			{
				LogManager.WriteWarning(this, this.ToString() + "Cannot register WebEntity, because Transaction state is: "+mState.ToString(), "Transaction");
			}
		}

		/// <summary>
		/// Registers an sqlEntity to the transaction
		/// </summary>
		/// <param name="sqlEntity">entity to register</param>
		public void RegisterSqlEntity(Entity sqlEntity)
		{
			if (sqlEntity == null)
			{
				throw new ArgumentNullException("sqlEntity");
			}
            if (sqlEntity.ActionState == EntityActionState.Detached)
            {
                throw new ArgumentException("Cannot register SqlEntity, because it's detached.");
            }
            if (mState == TransactionState.Idle || mState == TransactionState.Started)
            {
                try
                {
                    if (!mSqlEntities.Contains(sqlEntity))
                    {
                        mSqlEntities.Add(sqlEntity);
                    }
                }
                catch (Exception ex)
                {
                    LogManager.WriteWarning(this, this.ToString() + ex.ToString(), "Transaction");
                    throw ex;
                }
            }
            else
            {
                LogManager.WriteWarning(this, this.ToString() + "Cannot register SqlEntity, because Transaction state is: " + mState.ToString(), "Transaction");
            }
		}

		/// <summary>
		/// Registers an SqlRepository to the transaction
		/// </summary>
		/// <param name="sqlRepository">SqlRepository to register</param>
        public void RegisterSqlRepository(SqlRepository<Entity> sqlRepository)
        {
            if (mState == TransactionState.Idle || mState == TransactionState.Started)
            {
                try
                {
                    if (!mSqlRepositories.Contains(sqlRepository))
                    {
                        mSqlRepositories.Add(sqlRepository);
						if (mState == TransactionState.Started)
						{
							sqlRepository.Provider = mProvider;
							if (mType == TransactionType.Standard)
							{
								sqlRepository.BeginTransaction();
							}
							if (mType == TransactionType.Distributed)
							{
								sqlRepository.OpenConnection();
							}
						}
                    }
                }
                catch (Exception ex)
                {
                    LogManager.WriteWarning(this, this.ToString() + ex.ToString(), "Transaction");
                    throw ex;
                }
            }
            else
            {
                LogManager.WriteWarning(this, this.ToString() + "Cannot register SqlRepository, because Transaction state is: " + mState.ToString(), "Transaction");
            }
        }

		/// <summary>
		/// Starts a new transaction
		/// </summary>
		/// <param name="startDistributedTransaction">True: starts distributed transaction (uses MSDTC)
		/// False: starts standard transaction</param>
		public void BeginTransaction(bool startDistributedTransaction)
		{
			if (startDistributedTransaction)
			{
				BeginDistributedTransaction();
			}
			else
			{
				BeginStandardTransaction();
			}
		}

		private void BeginStandardTransaction()
		{
			if (mState == TransactionState.Idle)
			{
				mType = TransactionType.Standard;
				foreach (IRepository<IEntity> webRepository in mWebRepositories)
				{
					webRepository.BeginTransaction();
				}
				foreach (SqlRepository<Entity> repository in mSqlRepositories)
				{
					repository.Provider = mProvider;
					repository.BeginTransaction();
				}
				mState = TransactionState.Started;
			}
			else
			{
				LogManager.WriteWarning(this, this.ToString() + "Cannot begin transaction, because Transaction state is: " + mState.ToString(), "Transaction");
			}
		}

		private void BeginDistributedTransaction()
		{
			if (mState == TransactionState.Idle)
			{
				mType = TransactionType.Distributed;

				mTransactionScope = new TransactionScope(TransactionScopeOption.Required, new TimeSpan(0, 10, 0));
				foreach (IRepository<IEntity> webRepository in mWebRepositories)
				{
					webRepository.BeginTransaction();
				}
				foreach (SqlRepository<Entity> repository in mSqlRepositories)
				{
					repository.Provider = mProvider;
					repository.OpenConnection();
				}
				mState = TransactionState.Started;
			}
			else
			{
				LogManager.WriteWarning(this, this.ToString() + "Cannot begin transaction, because Transaction state is: " + mState.ToString(), "Transaction");
			}
		}

		/// <summary>
		/// Commits transaction (either standard or distributed)
		/// </summary>
		public void Commit()
		{
			if (mState == TransactionState.Started)
			{
				foreach (KeyValuePair<IEntity, IRepository<IEntity>> pair in mWebEntities)
				{
					pair.Value.Commit(pair.Key);
					LogManager.WriteInfo(this, this.ToString() + string.Format(" Committed entity {0} using {1}.", pair.Key.ToString(), pair.Value), "Transaction");
				}
				switch (mType)
				{
                    default:
					case TransactionType.Standard:
						mProvider.CommitAll();
						mProvider = null;
						LogManager.WriteInfo(this, this.ToString() + " Standard transaction committed.", "Transaction");
						break;
					case TransactionType.Distributed:
                        mTransactionScope.Complete();
						mTransactionScope.Dispose();
						mProvider.CloseAllConnection();
						mTransactionScope = null;
						mProvider = null;
						LogManager.WriteInfo(this, this.ToString() + " Distributed transaction committed.", "Transaction");
						break;
				}
                foreach (Entity entity in mSqlEntities)
                {
					Entity.SetTransactionState(entity, EntityTransactionState.Committed);
                }
				foreach (KeyValuePair<IEntity, IRepository<IEntity>> pair in mWebEntities)
				{
					Entity.SetTransactionState(pair.Key, EntityTransactionState.Committed);
				}
				mState = TransactionState.Succeed;
			}
			else
			{
				LogManager.WriteWarning(this, this.ToString() + "Cannot commit transaction, because Transaction state is: " + mState.ToString(), "Transaction");
			}
		}

		/// <summary>
		/// Rollbacks transaction (either standard or distributed)
		/// </summary>
		public void Rollback()
		{
			if (mState == TransactionState.Started)
			{
				mState = TransactionState.Failed;
				foreach (KeyValuePair<IEntity, IRepository<IEntity>> pair in mWebEntities)
				{
					pair.Value.Rollback(pair.Key);
					LogManager.WriteInfo(this, this.ToString() + string.Format(" Rolled back entity {0} using {1}.", pair.Key.ToString(), pair.Value), "Transaction");
				}
				switch (mType)
				{
                    default:
					case TransactionType.Standard:
						mProvider.RollbackAll();
						mProvider = null;
						LogManager.WriteInfo(this, this.ToString() + " Standard transaction rolled back.", "Transaction");
						break;
					case TransactionType.Distributed:
                        mTransactionScope.Dispose();
						mProvider.CloseAllConnection();
						mTransactionScope = null;
						mProvider = null;
						LogManager.WriteInfo(this, this.ToString() + " Distributed transaction rolled back.", "Transaction");
						break;
				}
                foreach (Entity entity in mSqlEntities)
                {
					Entity.SetTransactionState(entity, EntityTransactionState.Rollbacked);
				}
			}
			else
			{
				LogManager.WriteWarning(this, this.ToString() + "Cannot rollback transaction, because Transaction state is: " + mState.ToString(), "Transaction");
			}
		}

		#endregion

		#region Overriden methods

		/// <summary>
		/// Returns true if this transaction is equal to parameter obj
		/// </summary>
		/// <param name="obj">transaction with compare</param>
		/// <returns>true if equal</returns>
		public override bool Equals(object obj)
		{
			Transaction trn = obj as Transaction;
			if (trn != null)
			{
				return this.mTransactionId.Equals(trn.mTransactionId);
			}
			return false;
		}

		/// <summary>
		/// Returns hash code of transaction
		/// </summary>
		/// <returns></returns>
		public override int GetHashCode()
		{
			return base.GetHashCode();
		}

		/// <summary>
		/// Returns the string representation of the transaction
		/// </summary>
		/// <returns></returns>
		public override string ToString()
		{
			return System.String.Format("{0} (TransactionId = {1}, State = {2})", base.ToString(), mTransactionId.ToString(), mState.ToString());
		}

		#endregion

		#region Private helper methods

		private bool ContainsWebEntityPair(KeyValuePair<IEntity, IRepository<IEntity>> kvp)
		{
			return mWebEntities.Contains(kvp);
		}

		private void AddWebEntityPair(KeyValuePair<IEntity, IRepository<IEntity>> kvp)
		{
			if (!mWebRepositories.Contains(kvp.Value))
			{
                mWebRepositories.Add(kvp.Value);
                if (mState == TransactionState.Started)
                {
                    kvp.Value.BeginTransaction();
                }
			}
			mWebEntities.Add(kvp);
		}

		private KeyValuePair<IEntity, IRepository<IEntity>> GetWebKeyValuePair(IRepository<IEntity> repository, IEntity entity)
		{
			return new KeyValuePair<IEntity, IRepository<IEntity>>(entity, repository);
		}

        #endregion

	}
}